-
Odin does not have a Garbage Collector (GC).
Assignment
Copy
-
"
a = bmakes a copy?"-
It copies
bitself, but ifbis (or contains) a pointer, the data behind that pointer won't get cloned.
-
-
Pointers :
-
Pointers aren't magical. They're values that (can) point to other values.
-
-
Maps :
-
Keys and values are always copied.
-
-
Procedures :
-
Parameters:
-
Are always passed by copy.
-
-
Returns:
-
Copy or move?
-
-
Size
-
The word
sizeis used to denote the size in bytes . -
The word
lengthis used to denote the count of objects. -
size_of.-
This is evaluated at compile-time .
-
Takes an expression or type, and returns the size in bytes of the type of the expression if it was hypothetically instantiated as a variable.
-
The size does not include any memory possibly referenced by a value.
-
Slice :
-
This would return the size of the internal slice data structure and not the size of the memory referenced by the slice.
-
-
Struct :
-
Return size includes any padding introduced by field alignment (if not specified with
#packed).
-
-
Other types follow similar rules.
-
-
-
This is evaluated at runtime .
-
Returns the size of the type that the passed typeid represents
-
Memory Leaks
-
If the procedure does not free the memory automatically, then everything that had memory allocated must be returned from the procedure, otherwise we'll have a memory leak .
-
While inside a procedure, if I create something on the heap I should always return its pointer and not its value.
-
If you return by value, you're returning it on the stack; except any pointers that value may contain
-
The only way to reference allocated memory is by pointer (note that slices,
strings, etc., have pointers internally, so those count)
If a procedure allocates internally
-
Options:
-
Pass an allocator as one of the parameters and return the object that will need freeing.
-
Requiring an allocators is important to avoid "implicit allocations", which remove the agency from the user and makes easier to get memory leaks by accident, as it's not obvious something needs to be freed unless you read through the procedure implementation. C sometimes does this, which is bad.
-
Returning the allocated objects is a must to avoid memory leaks, otherwise the "handle" to the allocation is lost and you'll likely get a memory leak, unless the allocation was made using a Arena allocator, or similar, so by freeing the arena everything allocated with it is freed.
-
-
Create the object outside and pass the pointer to the object as a parameter.
-
This is a way to avoid a new object being created inside the procedure. It just modifies an existing object, which will know to delete.
-
-
Examples
-
Will leak.
create_data :: proc(allocator: mem.Allocator) -> (data: Data) { data_ptr := new(Data, allocator = allocator) data_ptr^ = { .. something } data = data_ptr^ return } my_data := create_data(context.allocator) free(&my_data)-
data = data_ptr^copies the data from the allocation back to the stack, and then the pointer to the allocation is forgotten.
-
-
Will not leak:
create_data :: proc(allocator: mem.Allocator) -> (data_ptr: ^Data) { data_ptr = new(Data, allocator = allocator) data_ptr^ = { .. something } return } my_data_ptr := create_data(context.allocator) free(my_data_ptr)
Stack-Use-After-Return
Pointer to a pointer on the stack
-
If I have a procedure that does
x: ^int = new_clone(123), if I return&x, is this a stack-use-after-return bug? -
Pointer or not,
xis still a local variable, so&xwould be a pointer to a pointer on the stack, yes. The thing thatxpoints to , however, is not.
Examples
x_proc :: proc() -> ^int {
x_value: int = 123
return &x_value
// `x` is a value stored in the stack, while `&x` is a pointer to a value stored in the stack; this is invalid.
// Compiler Error: It is unsafe to return the address of a local variable ('&x_value') from a procedure, as it uses the current stack frame's memory
}
a_proc :: proc() -> ^int {
a_slice := make([]int, 4, context.temp_allocator)
a_slice[2] = 30
return &a_slice[2]
// `a_slice[2]` is a value stored in the heap, while `&a_slice[2]` is a pointer to a value stored in the heap, so it's fine.
}
b_proc :: proc() -> (a: any) {
b_slice := make([]int, 4, context.temp_allocator)
b_slice[2] = 30
return b_slice[2]
// `b_slice[2]` is a value stored in the heap, while `a: any = &b_slice[2]` which is a pointer to a value stored in the heap, so it's fine.
}
c_proc :: proc() -> (a: any) {
c_slice := make([]int, 4, context.temp_allocator)
c_slice[2] = 30
return &c_slice[2]
// `&c_slice[2]` is a pointer to a value stored in the heap, but `any` created an implicit indirection with `_tmp`.
// So, this ends up being `c.data = &_tmp`, where `_tmp` is in the stack of `c_proc`, so this is invalid.
}
main :: proc() {
a := a_proc()
fmt.printfln("a: %v", a) // prints an address
fmt.printfln("a^: %v", a^) // prints '30'
b := b_proc()
fmt.printfln("b: %v", b) // prints '30'
fmt.printfln("b.(): %v", b.(int)) // prints '30'
fmt.printfln("b.data: %v", b.data) // prints an address
// fmt.printfln("b.data^: %v", b.data^) // Not possible to dereference rawptr.
c := c_proc()
fmt.printfln("c: %v", c) // Invalid. This is accessing invalid memory; ASan doesn't crash, but it should.
fmt.printfln("c^: %v", c.(^int)) // Invalid. This is accessing invalid memory; ASan doesn't crash, but it should.
// Barinzaya: I wonder if `any`s aren't integrated with ASan.
}
Use-After-Free (UAF)
Rules against UAF
-
I should not create an object inside a procedure and store its address somewhere.
-
As soon as the procedure ends, its address will no longer exist.
-
Even if you return the object by address, its address will change once it leaves the procedure's stack.
-
See the example 'Question: Tracking allocator doesn't work' for more explanation.
-
Address after free
-
Doesn't change...
int_ptr := new(int)
fmt.println(int_ptr) // 0x262CC7B6518
free(int_ptr)
fmt.println(int_ptr) // 0x262CC7B6518
Question: Tracking allocator doesn't work
-
Caio:
track := init_tracking_allocator() init_tracking_allocator :: proc() -> mem.Tracking_Allocator { track: mem.Tracking_Allocator mem.tracking_allocator_init(&track, context.allocator) context.allocator = mem.tracking_allocator(&track) return track } -
Barinzaya:
-
Changes to
contextare scoped , so afterinit_tracking_allocatorreturns,context.allocatordoes not change in the caller. -
The
Allocatorcontains a pointer to the underlying allocator data, which is on the stack ininit_tracking_allocator(i.e. it's&track) and would no longer be valid after that proc returns. Returning it will move it, and invalidate the pointer.-
any time you use
&on a local variable, the resulting pointer is only valid until the proc that variable is in returns. When you hand out a pointer (i.e. tomem.tracking_allocator), you need to be aware of how long that pointer needs to remain valid, and make sure that it's long enough
-
-
-
Caio:
-
wow, that sounds crazy hard to debug, I mean, sure with practice that comes natural, but how can I check for a reference to a pointer used like that? That's been my question for today. What I mean is, I wish there was a way to make such bugs not silent, because for what it seems, it just corrupts the data without giving any indication of such. There was a lot of suggestions to use a debugger or address sanitization, but both this suggestions require me to be actively looking for something, and what scares me is that I'm not good enough with memory to know when this will happen.
-
-
Tekk:
-
i saw a project like toybox actually use this behavior to record the beginning of the stack, so this kind of bug isnt something a compiler can check for without being extremely annoying. just like in rust, you can cause memory leaks by forgetting the root node of a linked list; the compiler has no idea what's your intention behind that.
-
plus, maybe youre creating a small buffer on the stack, so you might actually want an address to a local variable to pass to a procedure
-
-
Barinzaya:
-
It does become natural with practice, but there's just no sure-fire way to catch use-after-free issues that doesn't require actively looking for them. Odin is unmanaged, and memory is, fundamentally, just a large array of bytes, it has no concept of who owns it or what it contains beyond "bytes". The higher-level concepts that we're used to are just a matter of how those bytes are treated
-
The trick often comes down to just making your own life easier. Keep things in arrays, rather than separate allocations, use arenas for things that you know have a limited life-time (particularly deeply-nested structures that you know you'll destroy all at once, but can also be good for e.g. "I won't need this after this frame ends", for instance). These practices are better not only for you to keep track of, but less work for the CPU to do as well
-
-
jason:
-
I can attest to what Barinzaya said. It does become natural. I can write an entire program start to finish without making that mistake or really giving it any thought.
-
-
Aunt Esther:
-
I may not understand your issue totally, but if you set up ASAN correctly it catches all the below. Not sure there is anything left to check for. Multi-pointer bounds checks are not covered since you are in C-like territory there, but use those with extreme caution and usually for FFI. IMO most people do not understand these four points and options for ASAN use on windows with Odin -- you CAN use ASAN to detect:
-
Heap variable use after free (UAF) -- Odin does not detect this at compile or runtime.
-
Stack (local) variable use after free (UAF) at compile time for local intermediate variables assigned to local variables addresses of pointy local variables, e.g. taking the address of an indexed local variable like a local fixed array and assigning to another local variable for return -- Odin does not catch these at compile OR runtime. (note, Odin will error at compile time for local pointer type variables (e.g. fixed array) that directly have their address used as a return value). Historically stack UAF can sometimes prevent false positives (hence the default false setting), but so far it has not in my experience with Odin.
-
For all stack UAF detection, you have to set the ASAN variable
detect_stack_use_after_returntotruebefore you compile (default value iffalseotherwise) - see below for an example build command to actuate these stack UAF features. -
Compile time bounds checks for runtime type violations on both the stack and heap. Odin will catch this class of bugs only at runtime. For example a called proc accesses a runtime variable out of bounds.
-
-
Here is an example Odin compiler build command for windows
-
set ASAN_OPTIONS=detect_stack_use_after_return=true & odin run . -debug -warnings-as-errors -sanitize:address -vet-unused-variables -vet-unused-imports -vet-shadowing -vet-style -strict-style -vet-semicolon -out:output.exe
-
-
The above for UAF and bounds checks, plus a debugger (for pinpointing) and the tracking allocators (for leaks and bad/double frees) should cover a lot.
-
-
Correct code:
track := init_tracking_allocator()
context.allocator = mem.tracking_allocator(&track)
init_tracking_allocator :: proc() -> mem.Tracking_Allocator {
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
return track
}